#include "GLCanvas.h"
#include <map>
#include <thread>

#ifdef _DEBUG
#define CheckGL checkGL
#else
#define CheckGL (void)
#endif

const GLchar* fs_tex_rect =
"#version 330\n"
"uniform sampler2DRect tex;\n"
"out vec4 fColor;\n"
"void main() {\n"
"    fColor = texture(tex, gl_FragCoord.xy);\n"
"}\n";

const GLchar* fs_tex_2d = 
"#version 330\n"
"uniform sampler2D tex;\n"
"uniform vec2 rect;\n"
"out vec4 fColor;\n"
"void main() {\n"
"    fColor = texture(tex, gl_FragCoord.xy/rect);\n"
"}\n";

static std::map<GLenum, std::string> GLErrorMap = {
{ GL_NO_ERROR, "GL_NO_ERROR" },
{ GL_INVALID_ENUM, "GL_INVALID_ENUM" },
{ GL_INVALID_VALUE, "GL_INVALID_VALUE" },
{ GL_INVALID_OPERATION, "GL_INVALID_OPERATION" },
{ GL_STACK_OVERFLOW, "GL_STACK_OVERFLOW" },
{ GL_STACK_UNDERFLOW, "GL_STACK_UNDERFLOW" },
{ GL_OUT_OF_MEMORY, "GL_OUT_OF_MEMORY" },
{ GL_INVALID_FRAMEBUFFER_OPERATION,"GL_INVALID_FRAMEBUFFER_OPERATION" },
{ GL_CONTEXT_LOST, "GL_CONTEXT_LOST" }
};

static inline void checkGL(int lineNo)
{
    auto err = glGetError();
    if (err == GL_NO_ERROR)
        return;
    CPrintf(enmCFC_Yellow, enmCBC_Black, "%d : %d, %s\n", lineNo, err, GLErrorMap[err].c_str());
}

GLCanvas::GLCanvas()
{
}

GLCanvas::~GLCanvas()
{
    release();
}

void GLCanvas::init(int w, int h, int num_pbo)
{
    release();

    _winW = w;
    _winH = h;
    _pbo.resize(num_pbo);

    glClearColor(1, 1, 1, 1); CheckGL(__LINE__);
    glViewport(0, 0, _winW, _winH); CheckGL(__LINE__);

    GLuint size = BytesPerPixel * _winW * _winH;

    _data.resize(size);

    glGenBuffers(num_pbo, &_pbo[0]); CheckGL(__LINE__);
    for (int i = 0; i < num_pbo; ++i)
    {
        glBindBuffer(GL_PIXEL_UNPACK_BUFFER, _pbo[i]); CheckGL(__LINE__);
        glBufferData(GL_PIXEL_UNPACK_BUFFER, size, 0, GL_STREAM_DRAW); CheckGL(__LINE__);
    }
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, NULL); CheckGL(__LINE__);

    glGenTextures(1, &_tid); CheckGL(__LINE__);
    glBindTexture(TexTarget, _tid); CheckGL(__LINE__);
    glTexImage2D(TexTarget, 0, TexFormat, _winW, _winH, 0, TexFormat, GL_UNSIGNED_BYTE, 0); CheckGL(__LINE__);
    glTexParameteri(TexTarget, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(TexTarget, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(TexTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(TexTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glBindTexture(TexTarget, NULL); CheckGL(__LINE__);

    const GLchar* fsStr = TexTarget == GL_TEXTURE_RECTANGLE ? fs_tex_rect : fs_tex_2d;

    GLuint fs = glCreateShader(GL_FRAGMENT_SHADER); CheckGL(__LINE__);
    glShaderSource(fs, 1, &fsStr, NULL); CheckGL(__LINE__);
    glCompileShader(fs); CheckGL(__LINE__);
    GLint param = 0;
    glGetShaderiv(fs, GL_COMPILE_STATUS, &param); CheckGL(__LINE__);
    if (GL_TRUE != param)
    {
        char buf[4096];
        glGetShaderInfoLog(fs, 4096, NULL, buf); CheckGL(__LINE__);
        CPrint(buf, enmCFC_Red);
        glDeleteShader(fs);
        return;
    }

    _program = glCreateProgram(); CheckGL(__LINE__);
    glAttachShader(_program, fs); CheckGL(__LINE__);
    glLinkProgram(_program); CheckGL(__LINE__);
    glGetProgramiv(_program, GL_LINK_STATUS, &param); CheckGL(__LINE__);
    if (GL_TRUE != param)
    {
        char buf[4096];
        glGetProgramInfoLog(_program, 4096, NULL, buf); CheckGL(__LINE__);
        CPrint(buf, enmCFC_Red);
        glDeleteShader(fs);  CheckGL(__LINE__);
        glDeleteProgram(_program);   CheckGL(__LINE__);
        return;
    }

    GLfloat vertexData[] = { -1,1,0, -1,-1,0, 1,1,0, 1,-1,0 };
    glGenVertexArrays(1, &_vao); CheckGL(__LINE__);
    glBindVertexArray(_vao); CheckGL(__LINE__);
    glGenBuffers(1, &_vbo); CheckGL(__LINE__);
    glBindBuffer(GL_ARRAY_BUFFER, _vbo); CheckGL(__LINE__);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertexData), vertexData, GL_STATIC_DRAW); CheckGL(__LINE__);
    glBindBuffer(GL_ARRAY_BUFFER, _vbo); CheckGL(__LINE__);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0); CheckGL(__LINE__);
    glEnableVertexAttribArray(0); CheckGL(__LINE__);
    glBindVertexArray(NULL); CheckGL(__LINE__);
    glBindBuffer(GL_ARRAY_BUFFER, NULL); CheckGL(__LINE__);

    glUseProgram(_program); CheckGL(__LINE__);
    glUniform1i(glGetUniformLocation(_program, "tex"), 0); CheckGL(__LINE__);
    glUniform2f(glGetUniformLocation(_program, "rect"), (GLfloat)_winW, (GLfloat)_winH); CheckGL(__LINE__);
    
    _valid = true;
}

void GLCanvas::release()
{
    glUseProgram(NULL);
    glBindVertexArray(NULL);
    glBindBuffer(GL_ARRAY_BUFFER, NULL);
    glBindTexture(TexTarget, NULL);
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, NULL);

    if(glIsProgram(_program)) 
        glDeleteProgram(_program);
    CheckGL(__LINE__);
    
    if (glIsBuffer(_vbo)) 
        glDeleteBuffers(1, &_vbo);
    CheckGL(__LINE__);
    
    if (glIsVertexArray(_vao))
        glDeleteVertexArrays(1, &_vao);
    CheckGL(__LINE__);
    
    if (glIsTexture(_tid))
        glDeleteTextures(1, &_tid);
    CheckGL(__LINE__);

    if (_pbo.size()>0 && glIsBuffer(_pbo[0]))
        glDeleteBuffers(_pbo.size(), &_pbo[0]);
    CheckGL(__LINE__);
    
    _vbo = 0;
    _vao = 0;
    _tid = 0;
    _program = 0;
    _pboIndex = 0;
    _pbo.clear();
    _data.clear();
    _valid = false;
}

void GLCanvas::render()
{
    if (!_valid) return;

    if (_dirty) 
    {
        GLuint nxt = (_pboIndex + 1) % _pbo.size();
        glBindBuffer(GL_PIXEL_UNPACK_BUFFER, _pbo[nxt]); CheckGL(__LINE__);

        glBufferData(GL_PIXEL_UNPACK_BUFFER, _data.size(), NULL, GL_STREAM_DRAW);
        void* buffer = glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY); CheckGL(__LINE__);
        memcpy(buffer, _data.data(), _data.size());
        glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER); CheckGL(__LINE__);

        glBindBuffer(GL_PIXEL_UNPACK_BUFFER, NULL); CheckGL(__LINE__);
        _pboIndex = nxt;
    }

    //glClear(GL_COLOR_BUFFER_BIT); CheckGL(__LINE__);
    glUseProgram(_program); CheckGL(__LINE__);
    glBindVertexArray(_vao); CheckGL(__LINE__);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); CheckGL(__LINE__);
    glBindVertexArray(NULL); CheckGL(__LINE__);

    if (_dirty) 
    {
        glBindTexture(TexTarget, _tid); CheckGL(__LINE__);
        glBindBuffer(GL_PIXEL_UNPACK_BUFFER, _pbo[_pboIndex]); CheckGL(__LINE__);
        glTexImage2D(TexTarget, 0, TexFormat, _winW, _winH, 0, TexFormat, GL_UNSIGNED_BYTE, 0); CheckGL(__LINE__);
        _dirty = false;
    }
}

void GLCanvas::resize(int w, int h)
{
    if (!_valid) return;

    _winW = w;
    _winH = h;
    glViewport(0, 0, _winW, _winH); CheckGL(__LINE__);

    GLuint size = BytesPerPixel * _winW * _winH;

    _data.resize(size);

    for (int i = 0; i < _pbo.size(); ++i)
    {
        glBindBuffer(GL_PIXEL_UNPACK_BUFFER, _pbo[i]); CheckGL(__LINE__);
        glBufferData(GL_PIXEL_UNPACK_BUFFER, size, NULL, GL_STREAM_DRAW); CheckGL(__LINE__);
    }
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, NULL); CheckGL(__LINE__);

    glBindTexture(TexTarget, _tid); CheckGL(__LINE__);
    glTexImage2D(TexTarget, 0, TexFormat, _winW, _winH, 0, TexFormat, GL_UNSIGNED_BYTE, NULL); CheckGL(__LINE__);
    glBindTexture(TexTarget, NULL); CheckGL(__LINE__);

    GLfloat rect[] = { (GLfloat)_winW, (GLfloat)_winH };
    glUniform2f(glGetUniformLocation(_program, "rect"), (GLfloat)_winW, (GLfloat)_winH); CheckGL(__LINE__);
}
